昨天,我們在 Xcode console 上一直看到這個 warning
ForEach<Array<String>, String, Text>: the ID q occurs multiple times within the collection, this will give undefined results!
在 List 那一段,我們使用 logs 本身當成 ID,而 logs 是型態為 [String],如果這個 array 裡面已經有 q,那再加上一個 q,就會有數個 q 了。而 ID 本身的意思就是「唯一值」,這時候值不唯一,就會噴出警告。(不過在 UI 上,仍然能正常顯示複數個 q,看來 List 有做其他的事情在 Array 更新的時候)
List(logs, id: \.self) { log in
let eachLog = "打出的字為: \(log)"
Text(eachLog)
}
看來 String 在這裡不是個很好的資料結構,我們設計一個 TypingLog 來放進 logs 吧。這個 TypingLog 為了要能被 List 呈現,他需要 conform Identifiable 和 Hashable 這兩個 protocol。
struct TypingLog: Identifiable, Hashable {
let id: UUID = .init()
let typedString: String
}
然後修改一下 logs 的型別
@State private var logs: [TypingLog] = []
再修改一下 list
List(logs, id: \.self) { log in
let eachLog = "打出的字為: \(log.typedString)"
Text(eachLog)
}
再改一下 timer
/// 猴子開始打字囉
private func askMonkeysTyping() {
typingTimer = Timer.publish(every: 0.1, on: .main, in: .common)
.autoconnect()
.sink { _ in
let typedCharacter = createRandomString()
print("發動產生文字: \(typedCharacter)")
// 如果想和 console 一樣,最新的在最下面,就用 append()
let typingLog = TypingLog(typedString: typedCharacter)
logs.insert(typingLog, at: 0)
}
}
然後,再 Build 起來讓猴子打個字,現在再也不會看到剛剛那個 warning 了。
整個 view 的程式碼如下
import SwiftUI
import Combine
struct InfiniteMonkeyTypingContentView: View {
@State private var targetText = ""
@State private var monkeyTyperCount = 1
@State private var logText = ""
@State private var textStyle = UIFont.TextStyle.body
/// 發動「猴子」打字的 timer
@State private var typingTimer: AnyCancellable?
@State private var logs: [TypingLog] = []
private var targetHint: String {
if targetText.isEmpty {
return "目前沒目標,請輸入目標文字在輸入框"
}
return "你的目標為: \(targetText)"
}
var body: some View {
VStack {
Text("無限猴子打字機")
.font(.largeTitle)
.padding(.top, 20)
Text(targetHint)
.lineLimit(1)
.padding()
TextField("請輸入目標", text: $targetText)
.autocapitalization(.none)
.padding()
.textFieldStyle(.roundedBorder)
monkeyTyperStepper
monkeyActionButtons
monkeyLogsAndClearLogs
List(logs, id: \.self) { log in
let eachLog = "打出的字為: \(log.typedString)"
Text(eachLog)
}
Spacer()
}
}
private var monkeyTyperStepper: some View {
HStack {
Stepper("猴子數: \(monkeyTyperCount)") {
stepperIncrease()
} onDecrement: {
stepperDecrease()
}
}
.padding()
}
private var monkeyActionButtons: some View {
HStack {
Button("猴子停手") {
stopMonkeysTyping()
}
Button("叫猴子開始打字囉") {
askMonkeysTyping()
}
}
.buttonStyle(.bordered)
.padding()
}
private var monkeyLogsAndClearLogs: some View {
HStack {
Spacer()
Text("猴子的打字紀錄")
Button {
// TODO: - 清掉打字
} label: {
Text("清除打字紀錄")
}
.padding(.leading, 20)
.buttonStyle(.bordered)
Spacer()
}
}
private func stepperIncrease() {
monkeyTyperCount += 1
}
private func stepperDecrease() {
if monkeyTyperCount > 1 {
monkeyTyperCount -= 1
}
}
/// 猴子開始打字囉
private func askMonkeysTyping() {
typingTimer = Timer.publish(every: 0.1, on: .main, in: .common)
.autoconnect()
.sink { _ in
let typedCharacter = createRandomString()
print("發動產生文字: \(typedCharacter)")
// 如果想和 console 一樣,最新的在最下面,就用 append()
let typingLog = TypingLog(typedString: typedCharacter)
logs.insert(typingLog, at: 0)
}
}
/// 叫猴子停手
private func stopMonkeysTyping() {
typingTimer?.cancel()
}
}
extension InfiniteMonkeyTypingContentView {
private var alphabet: [String] {
let characters = "abcdefghijklmnopqrstuvwxyz"
var chars: [String] = []
for char in characters {
chars.append(String(char))
}
return chars
}
private func createRandomString() -> String {
var resultString = ""
for _ in 0..<monkeyTyperCount {
resultString += alphabet.randomElement() ?? ""
}
return resultString
}
}